#include <cassert>
#include <cstdio>

#include <fstream>
#include <sstream>
#include <stdexcept>

#include "obj_loader.h"

using namespace client::obj_loader;

static inline std::istream& operator >> (std::istream& is, VertexAttrId& attrId)
{
    std::string word;
    
    is>>word;
    if(!is)
        return is;
    
    int vertexId, texCoordId, normalId;
    
    if(std::sscanf(word.c_str(), "%d/%d/%d", &vertexId, &texCoordId, &normalId) == 3)
        attrId = VertexAttrId(vertexId, texCoordId, normalId);
    
    else if(std::sscanf(word.c_str(), "%d//%d", &vertexId, &normalId) == 2)
        attrId = VertexAttrId(vertexId, 0, normalId);
    
    else if(std::sscanf(word.c_str(), "%d/%d", &vertexId, &texCoordId) == 2)
        attrId = VertexAttrId(vertexId, texCoordId);
    
    else if(std::sscanf(word.c_str(), "%d", &vertexId) == 1)
        attrId = VertexAttrId(vertexId);
    
    else
        is.setstate(std::ios::failbit);
    
    return is;
}

static void parseObjFile(std::string const& filePath, ParserCallback *callback)
{
    std::ifstream file(filePath.c_str());
    if(!file.is_open())
        throw std::runtime_error("File open failed!");
    
    std::string line;
    std::istringstream buf;
    
    int lineNo = 0;
    while(++lineNo, std::getline(file, line))
    {
        std::string::size_type lastPrintable = line.find_last_not_of("\t\n\r ");
        if(lastPrintable == std::string::npos)
            continue;
        if(line[0] == '#')
            continue;
        buf.clear();
        buf.str(line);
        std::string word;
        buf>>word;
        if(word == "v")
        {
            float x, y, z, w;
            
            buf >> x >> y >> z;
            if(!buf)
                throw std::runtime_error("Vertex format error!");
            
            if(buf >> w)
                callback->gotVertex(Vertex(x, y, z, w));
            else
                callback->gotVertex(Vertex(x, y, z));
        }
        else if(word == "vt")
        {
            float s, t, p;
            
            buf >> s >> t;
            if(!buf)
                throw std::runtime_error("TexCoord format error!");
            
            if(buf >> p)
                callback->gotTexCoord(TexCoord(s, t, p));
            else
                callback->gotTexCoord(TexCoord(s, t));
        }
        else if(word == "vn")
        {
            float x, y, z;
            
            buf >> x >> y >> z;
            if(!buf)
                throw std::runtime_error("Normal format error!");
            
            callback->gotNormal(Normal(x, y, z));
        }
        else if(word == "f")
        {
            std::vector<VertexAttrId> attrIds;
            VertexAttrId attrId(0, 0, 0);
            int vertexCount = 0;
            
            while(buf >> attrId)
            {
                attrIds.push_back(attrId);
                ++vertexCount;
            }
            if(!buf.eof() || vertexCount < 3)
                throw std::runtime_error("Face format error!");
            
            callback->gotFace(Face(attrIds.begin(), attrIds.end()));
        }
        else if(word == "mtllib")
        {
            std::string fileName;
            buf >> fileName;
            
            if(!buf)
                throw std::runtime_error("MtlLib format error!");
            
            callback->gotMtlLib(MtlLib(fileName));
        }
        else if(word == "o")
        {
            std::string objectName;
            buf >> objectName;
            
            if(!buf)
                throw std::runtime_error("Object format error!");
            
            callback->gotObject(Object(objectName));
        }
        else if(word == "g")
        {
            std::string groupName;
            buf >> groupName;
            
            if(!buf)
                throw std::runtime_error("Group format error!");
            
            callback->gotGroup(Group(groupName));
        }
        else if(word == "usemtl")
        {
            std::string mtlName;
            buf >> mtlName;
            
            if(!buf)
                throw std::runtime_error("Material format error!");
            
            callback->gotMaterial(Material(mtlName));
        }
        else if(word == "s")
        {
            std::string word;
            buf >> word;
            
            if(!buf)
                throw std::runtime_error("SmoothGroup format error!");
            
            if(word == "off")
                callback->gotSmoothGroup(SmoothGroup(SmoothGroup::SMOOTH_GRP_OFF));
            else
            {
                int smoothGroupId;
                if(std::sscanf(word.c_str(), "%d", &smoothGroupId) != 1)
                    throw std::runtime_error("SmoothGroup format error!");
                callback->gotSmoothGroup(SmoothGroup(smoothGroupId));
            }
        }
        else if(word == "l")
        {
            // TODO: What's this?
        }
        else
            throw std::runtime_error("Unknown tag appeared in obj file.");
    }
}

ObjectFile::ObjectFile(const std::string& filePath):
currentObject(""),
currentGroup(""),
currentMaterial(""),
currentSmoothGroup(SmoothGroup::SMOOTH_GRP_OFF)
{
    parseObjFile(filePath, this);
}

void ObjectFile::gotVertex(const Vertex& vertex)
{
    int id = vertexList.size();
    this->vertexList.push_back(vertex);
    
    this->objects     [currentObject]     .vertexIds.push_back(id);
    this->groups      [currentGroup]      .vertexIds.push_back(id);
    this->materials   [currentMaterial]   .vertexIds.push_back(id);
    this->smoothGroups[currentSmoothGroup].vertexIds.push_back(id);
}

void ObjectFile::gotTexCoord(const TexCoord& texCoord)
{
    int id = texCoordList.size();
    this->texCoordList.push_back(texCoord);
    
    this->objects     [currentObject]     .texCoordIds.push_back(id);
    this->groups      [currentGroup]      .texCoordIds.push_back(id);
    this->materials   [currentMaterial]   .texCoordIds.push_back(id);
    this->smoothGroups[currentSmoothGroup].texCoordIds.push_back(id);
}

void ObjectFile::gotNormal(const Normal& normal)
{
    int id = normalList.size();
    this->normalList.push_back(normal);
    
    this->objects     [currentObject]     .normalIds.push_back(id);
    this->groups      [currentGroup]      .normalIds.push_back(id);
    this->materials   [currentMaterial]   .normalIds.push_back(id);
    this->smoothGroups[currentSmoothGroup].normalIds.push_back(id);
}

void ObjectFile::gotFace(const Face& face)
{
    int id = faceList.size();
    this->faceList.push_back(face);
    
    this->objects     [currentObject]     .faceIds.push_back(id);
    this->groups      [currentGroup]      .faceIds.push_back(id);
    this->materials   [currentMaterial]   .faceIds.push_back(id);
    this->smoothGroups[currentSmoothGroup].faceIds.push_back(id);
}

void ObjectFile::gotMtlLib(const MtlLib& )
{
}

void ObjectFile::gotObject(const Object& object)
{
    this->currentObject = object;
}

void ObjectFile::gotGroup(const Group& group)
{
    this->currentGroup = group;
}

void ObjectFile::gotMaterial(const Material& material)
{
    this->currentMaterial = material;
}

void ObjectFile::gotSmoothGroup(const SmoothGroup& smoothGroup)
{
    this->currentSmoothGroup = smoothGroup;
}
